﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace NetAssist.Pooling
{
  /// <summary>
  /// Represents a pool class.
  /// </summary>
  /// <typeparam name="T">The type of items in the pool.</typeparam>
  public partial class Pool<T> : IDisposable where T : class
  {
    TimerCallback timerDelegate;
    Timer collector;

    private const int MIN_POOL_SIZE = 1;
    private const int MAX_POOL_SIZE = int.MaxValue;
    private const long DEFAULT_POOL_AGE = 10 * 60 * 1000;   //10 Minutes    

    private object locker;
    private HashSet<PoolItem> free;
    private Func<T> factory;

    private int minPoolSize;
    private int maxPoolSize;
    private long maxPoolAge;

    /// <summary>
    /// Instantiate a new pool instance.
    /// </summary>
    /// <param name="factory">A factory method that returns new item when the pool is empty and a client is trying to acquire a pooled item instance.</param>
    public Pool(Func<T> factory)
      : this(factory, MIN_POOL_SIZE)
    { }

    /// <summary>
    /// Instantiate a new pool instance.
    /// </summary>
    /// <param name="factory">A factory method that returns new item when the pool is empty and a client is trying to acquire a pooled item instance.</param>
    /// <param name="minPoolSize">The minimum size of the pool. While purging old items, the process stops when the pool size is as low as minPoolSize.</param>
    public Pool(Func<T> factory, int minPoolSize)
      : this(factory, minPoolSize, MAX_POOL_SIZE)
    { }

    /// <summary>
    /// Instantiate a new pool instance.
    /// </summary>
    /// <param name="factory">A factory method that returns new item when the pool is empty and a client is trying to acquire a pooled item instance.</param>
    /// <param name="minPoolSize">The minimum size of the pool. While purging old items, the process stops when the pool size is as low as minPoolSize.</param>
    /// <param name="maxPoolSize">The maximum size of the pool. once the size of the pool reach manPoolSize, released objects are discarded and not placed back in the pool.</param>
    public Pool(Func<T> factory, int minPoolSize, int maxPoolSize)
      : this(factory, minPoolSize, maxPoolSize, DEFAULT_POOL_AGE)
    { }

    /// <summary>
    /// Instantiate a new pool instance.
    /// </summary>
    /// <param name="factory">A factory method that returns new item when the pool is empty and a client is trying to acquire a pooled item instance.</param>
    /// <param name="minPoolSize">The minimum size of the pool. While purging old items, the process stops when the pool size is as low as minPoolSize.</param>
    /// <param name="maxPoolSize">The maximum size of the pool. once the size of the pool reach manPoolSize, released objects are discarded and not placed back in the pool.</param>
    /// <param name="maxPoolAge">The maximum age of a pooled object.</param>
    public Pool(Func<T> factory, int minPoolSize, int maxPoolSize, long maxPoolAge)
    {
      if (factory == null)
        throw new ArgumentNullException("factory", "A factory function must be provided.");

      if (minPoolSize < 1)
        minPoolSize = 1;
      if (maxPoolSize < minPoolSize)
        maxPoolSize = minPoolSize;

      this.minPoolSize = minPoolSize;
      this.maxPoolSize = maxPoolSize;
      this.maxPoolAge = maxPoolAge;

      this.locker = new object();
      this.factory = factory;
      this.free = new HashSet<PoolItem>();

      timerDelegate = new TimerCallback(this.Collect);
      collector = new Timer(timerDelegate, null, maxPoolAge, maxPoolAge / 2);
    }

    /// <summary>
    /// Gets an item from the pool.
    /// </summary>
    /// <returns>An item from the pool.</returns>
    public T Acquire()
    {
      lock (locker)
      {
        T item;
        PoolItem poolItem = free.FirstOrDefault();

        if (poolItem == null)
          item = factory();
        else
        {
          this.free.Remove(poolItem);
          item = poolItem.PooledItem;
        }

        return item;
      }
    }

    /// <summary>
    /// Return an item to the pool.
    /// </summary>
    /// <param name="pooledItem">The item to return.</param>
    public void Release(T pooledItem)
    {
      lock (locker)
      {
        if (this.free.Count < maxPoolSize)
        {
          PoolItem poolItem = new PoolItem(pooledItem);
          this.free.Add(poolItem);
        }
      }
    }

    private void Collect(object state)
    {
      this.Collect();
    }

    /// <summary>
    /// Force removal (collection) of expired items.
    /// </summary>
    public void Collect()
    {
      lock (locker)
      {
        List<PoolItem> garbage = free.Where(I => I.Age > this.maxPoolAge).ToList();
        foreach (PoolItem poolItem in garbage)
        {
          if (free.Count <= minPoolSize)
            break;

          poolItem.Dispose();
          this.free.Remove(poolItem);
        }
      }
    }

    /// <summary>
    /// Gets The pool size.
    /// </summary>
    public int PoolSize
    {
      get
      {
        lock (locker)
        {
          int actual = free.Count;
          return this.minPoolSize > actual ? this.minPoolSize : actual;
        }
      }
    }

    #region Cleanup

    /// <summary>
    /// Releases all pooled items.
    /// </summary>
    public void Dispose()
    {
      collector.Dispose();
      foreach (PoolItem pooledItem in free)
        pooledItem.Dispose();
      free.Clear();
      GC.SuppressFinalize(this);
    }

    #endregion
  }
}
